'use strict';

const EventEmitter = require('events').EventEmitter;
const randomString = require('random-string');
const logger = require('../../logger');
const InvalidStateError = require('../../errors').InvalidStateError;
const RTCSessionDescription = require('../RTCSessionDescription');
const sdpUtils = require('./sdpUtils');

const NEGOTIATION_NEEDED_DELAY = 500;

class RTCPeerConnectionCommon extends EventEmitter
{
	constructor(options)
	{
		super();
		this.setMaxListeners(Infinity);

		this._logger = logger(`webrtc:RTCPeerConnection:${options.peer.name}`);
		this._logger.debug('constructor() [options:%o]', options);

		// Options.
		this._options = options;

		// Peer instance.
		this._peer = options.peer;

		// Local RTCSessionDescription.
		this._localDescription = null;

		// Remote RTCSessionDescription.
		this._remoteDescription = null;

		// Initial signaling state.
		this._signalingState = 'stable';

		// Busy flag.
		this._busy = false;

		// Whether the initial offer has been created.
		this._initialOfferCreated = false;

		// Whether first SDP O/A has been done.
		this._initialNegotiationDone = false;

		// Negotiation needed flag.
		this._negotiationNeeded = false;

		// Negotiation timer to collect them all.
		this._negotiationNeededTimer = null;

		// createOffer options for the initial offer.
		this._initialCreateOfferOptions =
		{
			offerToReceiveAudio : 1,
			offerToReceiveVideo : 1
		};

		// SDP global fields.
		this._sdpGlobalFields =
		{
			id      : randomString({ letters: false, length: 16 }),
			version : 0
		};

		// Handle peer 'close' event.
		this._peer.on('close', (error) =>
		{
			this.emit('close', error);
		});

		// Handle peer 'newrtpsender' event once first SDP O/A is done.
		this._peer.on('newrtpsender', () =>
		{
			this._logger.debug('peer "newrtpsender" event');

			// Set negotiation flag (unless the initial offer was not yet created).
			if (this._initialOfferCreated)
				this._negotiationNeeded = true;

			// Try to renegotiate if the initial SDP O/A was already done.
			if (this._initialNegotiationDone)
				this._mayRenegotiate();
		});
	}

	get closed()
	{
		return this._peer.closed;
	}

	get peer()
	{
		return this._peer;
	}

	get localDescription()
	{
		return this._localDescription;
	}

	get remoteDescription()
	{
		return this._remoteDescription;
	}

	get signalingState()
	{
		return this._signalingState;
	}

	close()
	{
		this._logger.debug('close()');

		// Cancel the negotiation timer.
		clearTimeout(this._negotiationNeededTimer);

		this._peer.close();
	}

	/**
	 * Set the peer's capabilities.
	 * @param {string} sdp - SDP offer generated by the remote peer.
	 */
	setCapabilities(sdp)
	{
		this._logger.debug('setCapabilities()');

		if (this.closed)
			throw new InvalidStateError('closed');

		if (this._peer.capabilities)
			throw new InvalidStateError('capabilities are ready set');

		let desc;

		try
		{
			desc = new RTCSessionDescription({ type: 'offer', sdp: sdp });
		}
		catch (error)
		{
			return Promise.reject(new Error(`invalid capabilities SDP: ${error}`));
		}

		// Set busy flag.
		this._busy = true;

		return Promise.resolve()
			// First set peer's capabilities.
			.then(() =>
			{
				let capabilities = sdpUtils.descToCapabilities(desc.parsed);

				return this._peer.setCapabilities(capabilities);
			})
			// Then create a Transport instance.
			.then(() =>
			{
				return this._peer.createTransport(this._options.transportOptions);
			})
			.then(() =>
			{
				this._logger.debug('setCapabilities() | succeed');

				// Unset busy flag.
				this._busy = false;
			})
			.catch((error) =>
			{
				this._logger.error('setCapabilities() | failed: %s', error);

				// Unset busy flag.
				this._busy = false;

				throw error;
			});
	}

	createOffer(options)
	{
		this._logger.debug('createOffer() [options:%o]');

		// Just for the initial SDP O/A.
		if (!this._initialNegotiationDone && options)
		{
			if (options.hasOwnProperty('offerToReceiveAudio'))
				this._initialCreateOfferOptions.offerToReceiveAudio = options.offerToReceiveAudio;

			if (options.hasOwnProperty('offerToReceiveVideo'))
				this._initialCreateOfferOptions.offerToReceiveVideo = options.offerToReceiveVideo;
		}

		if (this._busy)
			return Promise.reject(new InvalidStateError('busy'));

		if (!this._peer.capabilities)
			return Promise.reject(new InvalidStateError('capabilities not yet set'));

		if (this._signalingState !== 'stable')
		{
			return Promise.reject(new InvalidStateError(`invalid signaling state [signalingState:${this._signalingState}]`));
		}

		// Set busy flag.
		this._busy = true;

		return this._setUpOffer()
			.then(() =>
			{
				this._logger.debug('createOffer() | succeed');

				// Unset busy flag.
				this._busy = false;

				// Create an offer.
				let localDescription = this._createLocalDescription('offer');

				// Update flag.
				this._initialOfferCreated = true;

				// Resolve with it.
				return Promise.resolve(localDescription);
			})
			.catch((error) =>
			{
				this._logger.error('createOffer() | failed: %s', error);

				// Unset busy flag.
				this._busy = false;

				throw error;
			});
	}

	createAnswer()
	{
		this._logger.debug('createAnswer()');

		if (this._busy)
			return Promise.reject(new InvalidStateError('busy'));

		if (this._signalingState !== 'have-remote-offer')
		{
			return Promise.reject(new InvalidStateError(`invalid signaling state [signalingState:${this._signalingState}]`));
		}

		// Create an answer.
		let localDescription = this._createLocalDescription('answer');

		// Resolve with it.
		return Promise.resolve(localDescription);
	}

	/**
	 * NOTE: This method assumes that the given desc is the one previously
	 * created by createOffer() or createAnswer().
	 */
	setLocalDescription(desc)
	{
		this._logger.debug('setLocalDescription()');

		if (this._busy)
			return Promise.reject(new InvalidStateError('busy'));

		if (!this._peer.capabilities)
			return Promise.reject(new InvalidStateError('capabilities not yet set'));

		let remoteDescription = desc;
		let newSignalingState;

		switch (remoteDescription.type)
		{
			case 'offer':
			{
				if (this._signalingState !== 'stable')
					return Promise.reject(new InvalidStateError(`invalid RTCSessionDescription.type [type:${remoteDescription.type}, signalingState:${this._signalingState}]`));

				newSignalingState = 'have-local-offer';

				break;
			}

			case 'answer':
			{
				if (this._signalingState !== 'have-remote-offer')
					return Promise.reject(new InvalidStateError(`invalid RTCSessionDescription.type [type:${remoteDescription.type}, signalingState:${this._signalingState}]`));

				newSignalingState = 'stable';

				break;
			}

			default:
			{
				return Promise.reject(new Error(`invalid RTCSessionDescription.type [type:${remoteDescription.type}]`));
			}
		}

		// Update the local description.
		this._localDescription = desc;

		// Update signaling state.
		this._setSignalingState(newSignalingState);

		// Resolve.
		return Promise.resolve();
	}

	setRemoteDescription(desc)
	{
		this._logger.debug('setRemoteDescription()');

		if (this._busy)
			return Promise.reject(new InvalidStateError('busy'));

		if (!this._peer.capabilities)
			return Promise.reject(new InvalidStateError('capabilities not yet set'));

		let remoteDescription;

		try
		{
			remoteDescription = new RTCSessionDescription(desc);
		}
		catch (error)
		{
			return Promise.reject(new Error(`invalid RTCSessionDescriptionInit: ${error}`));
		}

		switch (remoteDescription.type)
		{
			case 'offer':
			{
				if (this._signalingState !== 'stable')
				{
					return Promise.reject(new InvalidStateError(`invalid RTCSessionDescription.type [type:${remoteDescription.type}, signalingState:${this._signalingState}]`));
				}

				// Initial offer received from the client.
				if (!this._remoteDescription)
				{
					return Promise.reject(new Error('initial offer from the endpoint not supported'));
				}
				// Re-offer received from the client.
				else
				{
					return Promise.reject(new Error('re-offer from the endpoint not supported'));
				}
			}

			case 'answer':
			{
				if (this._signalingState !== 'have-local-offer')
				{
					return Promise.reject(new InvalidStateError(`invalid RTCSessionDescription.type [type:${remoteDescription.type}, signalingState:${this._signalingState}]`));
				}

				// Initial answer received from the client.
				if (!this._remoteDescription)
				{
					// Set busy flag.
					this._busy = true;

					return this._handleRemoteInitialAnswer(remoteDescription)
						.then(() =>
						{
							this._logger.debug('setRemoteDescription() | succeed');

							// Unset busy flag.
							this._busy = false;

							// Set remote description.
							this._remoteDescription = remoteDescription;

							// Initial SDP O/A done.
							this._initialNegotiationDone = true;

							// Update signaling state.
							this._setSignalingState('stable');
						})
						.catch((error) =>
						{
							this._logger.error('setRemoteDescription() | failed: %s', error);

							// Unset busy flag.
							this._busy = false;

							throw error;
						});
				}
				// Re-answer received from the client.
				else
				{
					// Set busy flag.
					this._busy = true;

					return this._handleRemoteReAnswer(remoteDescription)
						.then(() =>
						{
							this._logger.debug('setRemoteDescription() | succeed');

							// Unset busy flag.
							this._busy = false;

							// Set remote description.
							this._remoteDescription = remoteDescription;

							// Update signaling state.
							this._setSignalingState('stable');
						})
						.catch((error) =>
						{
							this._logger.error('setRemoteDescription() | failed: %s', error);

							// Unset busy flag.
							this._busy = false;

							throw error;
						});
				}
			}

			default:
			{
				return Promise.reject(new Error(`invalid RTCSessionDescription.type [type:${remoteDescription.type}]`));
			}
		}
	}

	reset()
	{
		this._logger.debug('reset()');

		// Reset signalingState.
		this._signalingState = 'stable';

		// Unset busy flag.
		this._busy = false;
	}

	consumeIceRestart()
	{
		this._logger.debug('consumeIceRestart()');

		if (!this._localDescription)
			return Promise.reject(new InvalidStateError('no localDescription'));

		// Create an answer.
		let localDescription = this._createLocalDescription('answer');

		// Resolve with it.
		return Promise.resolve(localDescription);
	}

	_mayRenegotiate()
	{
		// If already scheduled, ignore.
		if (this._negotiationNeededTimer)
			return;

		// If busy, ignore.
		if (this._busy)
			return;

		// Ignore if capabilities are not yet set.
		if (!this._peer.capabilities)
			return;

		// Ignore if signalingState is not 'stable'.
		if (this._signalingState !== 'stable')
			return;

		// Schedule the task.
		this._negotiationNeededTimer = setTimeout(() =>
		{
			this._negotiationNeededTimer = null;

			// Ignore if closed.
			if (this.closed)
				return;

			// If busy, ignore.
			if (this._busy)
				return;

			if (this._negotiationNeeded)
			{
				// Reset flag.
				this._negotiationNeeded = false;

				// Emit event.
				this.emit('negotiationneeded');
			}
		}, NEGOTIATION_NEEDED_DELAY);
	}

	_setSignalingState(signalingState)
	{
		this._logger.debug('_setSignalingState() [signalingState:%s]', signalingState);

		if (this._signalingState === signalingState)
			return;

		this._signalingState = signalingState;

		// Emit 'signalingstatechange'.
		this.emit('signalingstatechange');

		// Things may have happened while busy, so check it.
		if (this._signalingState === 'stable')
		{
			process.nextTick(() =>
			{
				this._mayRenegotiate();
			});
		}
	}

	/**
	 * To be implemented by child classes.
	 */
	_setUpOffer()
	{

	}

	/**
	 * To be implemented by child classes.
	 */
	_handleRemoteInitialAnswer(remoteDescription) // eslint-disable-line no-unused-vars
	{

	}

	/**
	 * To be implemented by child classes.
	 */
	_handleRemoteReAnswer(remoteDescription) // eslint-disable-line no-unused-vars
	{

	}

	/**
	 * To be implemented by child classes.
	 */
	_createLocalDescription(type) // eslint-disable-line no-unused-vars
	{

	}
}

module.exports = RTCPeerConnectionCommon;
